/*
 * ecx_dom.c
 * cezanne@clunix,com, 2005.12
 *
 * Copyright (c) 2001-2005 by Clunix Inc. All Rights Reserved.
 */

#include "config.h"

#include "edbd.h"
#include "edbd_ecx.h"
#include "edbd_conn.h"

typedef struct _ecx_dom_t {
	ecx_info_t	*ecx_info;
	struct _ecx_dom_t	*parent;
	ECXATTR_t	ATTR;
	ecx_domtype_t	type;
	char		*ID;
	char		*name;
	char		*STAGE;
	ecm_ptrs_t	*childs;
	ecm_ptr_t	priv;
} ecx_dom_t;

typedef struct {
	ecm_ptrs_t	*doms;
	ecm_ptrs_t	*dom_roots;
	ECXDOM_t	dom_cur;
	ecm_uint32	dom_seq;
	sr_t		*sr_dom_names;
} DOMinfo_t;

typedef struct {
	ECMBOOL		block_level;
	ECMBOOL		recursive;
	ECMBOOL		applicable;
	ECMBOOL		dynamic;
	ecx_domtype_t	upper_dom;
	void (*print)(ecx_dom_t *);
	void (*free)(ecm_ptr_t priv);
	const char	*tagname;
} domspec_t;

typedef enum {
	FRULE_NONEMPTY,
	FRULE_INT,
	FRULE_FLOAT
} FIELD_rule_type_t;

typedef struct {
	FIELD_rule_type_t	type;
	ECMBOOL	has_min, has_max;

	union {
		struct {
			ecm_int64	min, max;
		} irange;
		struct {
			ecm_double	min, max;
		} drange;
	} range;
} FIELD_rule_t;

typedef struct {
	char	*value, *desc, *href;
	FIELD_rule_t	*rule;
} FIELD_priv_t;

typedef struct {
	char	*embid, *version;
} OBJECT_priv_t;

typedef struct {
	char	*name, *value;
} PARAM_priv_t;

typedef struct {
	ECMBOOL submit_all;
	ECMBOOL	multiple;
	ecx_dom_t	*selected;
} SELECT_priv_t;

typedef struct {
	SELECT_priv_t	*ppriv_sel;
	char	*value;
} OPTION_priv_t;

typedef struct {
	char	*empty_desc;
	char	*empty_selection_desc;
} GROUP_priv_t;

typedef struct {
	char	*DOM_ID, *desc;
} PASSWORD_priv_t;

typedef struct {
	ECMBOOL	enabled;
} DYNAMIC_priv_t;

static void print_children(ecx_dom_t *dom);
static ecx_dom_t *get_parent_dom(ecx_info_t *pinfo, ecx_domtype_t type);

static ECMBOOL
is_MSIE(ecx_info_t *pinfo)
{
	const char	*useragent;

	useragent = sr_getc_str(pinfo->sr_ecx_state, "USERAGENT");
	if (ecm_strcmp(useragent, "MSIE") == 0)
		return ECM_TRUE;
	return ECM_FALSE;
}

static void
print_UNKNOWN(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;
	char	*buf = dom->priv;

	fprintf(ecx_info->fp, "%s", buf);
}

static void
print_FORM(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;
	char		*value = dom->priv;

	fprintf(ecx_info->fp, "<input type=hidden name=FORM value=%s>\n", value);
	print_children(dom);
}

static void
print_GROUP(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;
	GROUP_priv_t	*priv = dom->priv;

	print_children(dom);
	if (!ecm_empty_str(priv->empty_desc) ||
	    !ecm_empty_str(priv->empty_selection_desc)) {
		fprintf(ecx_info->fp, "<input type=hidden name=RFC value=");
		fprintf(ecx_info->fp, "\"RFC_Group_nonempty('%s','%s', '%s')\">\n",
			dom->ID,
			ecm_safe_str(priv->empty_desc),
			ecm_safe_str(priv->empty_selection_desc));
	}
}

static void
free_GROUP(ecm_ptr_t priv)
{
	GROUP_priv_t	*ppriv = (GROUP_priv_t *)priv;

	ecm_free(ppriv->empty_desc);
	ecm_free(ppriv->empty_selection_desc);
	ecm_free(ppriv);
}

static void
print_FIELD(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;
	FIELD_priv_t	*priv;
	FIELD_rule_t	*rule;
	char	*value;

	priv = dom->priv;
	fprintf(ecx_info->fp, "<input type=field name=%s id=%s", dom->ID, dom->ID);
	value = priv->value;
	if (value)
		fprintf(ecx_info->fp, " value=\"%s\"", value);
	attr_print(ecx_info, dom->ATTR);
	if (priv->href) {
		if (is_MSIE(ecx_info)) {
			fprintf(ecx_info->fp, " onkeyup=\"javascript:if (window.event.keyCode == 13) %s\"", priv->href);
		}
		else
			fprintf(ecx_info->fp, " OnChange=%s", priv->href);
	}
	fprintf(ecx_info->fp, ">\n");

	if ((rule = priv->rule)) {
		char	*min = NULL, *max = NULL;
		char	*rule_value = NULL;

		switch (rule->type) {
		case FRULE_NONEMPTY:
			ecm_asprintf(&rule_value, "RFC_nonempty('%s','%s')", dom->ID, ecm_safe_str(priv->desc));
			break;
		case FRULE_INT:
			if (rule->has_min)
				ecm_asprintf(&min, ",%I64d", rule->range.irange.min);
			else
				min = ecm_strdup(",null");
			if (rule->has_max)
				ecm_asprintf(&max, ",%I64d", rule->range.irange.max);
			else
				max = ecm_strdup(",null");
			ecm_asprintf(&rule_value, "RFC_int('%s','%s'%s%s)",
				     dom->ID, ecm_safe_str(priv->desc), min, max);
			break;
		case FRULE_FLOAT:
			if (rule->has_min)
				ecm_asprintf(&min, ",%f", rule->range.drange.min);
			else
				min = ecm_strdup(",null");
			if (rule->has_max)
				ecm_asprintf(&max, ",%f", rule->range.drange.max);
			else
				max = ecm_strdup(",null");
			ecm_asprintf(&rule_value, "RFC_float('%s','%s'%s%s)",
				     dom->ID, ecm_safe_str(priv->desc), min, max);
			break;
		default:
			break;
		}
		if (rule_value)
			fprintf(ecx_info->fp, "<input type=hidden name=RFC value=\"%s\">", rule_value);
		ecm_free(min);
		ecm_free(max);
		ecm_free(rule_value);
	}
}

static void
free_FIELD(ecm_ptr_t priv)
{
	FIELD_priv_t	*ppriv = (FIELD_priv_t *)priv;

	ecm_free(ppriv->value);
	ecm_free(ppriv->desc);
	ecm_free(ppriv->rule);
	ecm_free(ppriv->href);
	ecm_free(ppriv);
}

static void
print_TEXTAREA(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;

	fprintf(ecx_info->fp, "<textarea name=%s id=%s",
		dom->ID, dom->ID);
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">%s</textarea>",
		ecm_safe_str((char *)dom->priv));
}

static void
print_PASSWORD(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;
	PASSWORD_priv_t	*priv;

	priv = (PASSWORD_priv_t *)dom->priv;
	fprintf(ecx_info->fp, "<input type=password name=%s id=%s", dom->ID, dom->ID);
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">\n");
	if (priv) {
		fprintf(ecx_info->fp, "<input type=hidden name=RFC value=\"RFC_password_match('%s','%s','%s')\">\n",
			priv->DOM_ID, dom->ID, ecm_safe_str(priv->desc));
	}
}

static void
free_PASSWORD(ecm_ptr_t priv)
{
	PASSWORD_priv_t	*ppriv = (PASSWORD_priv_t *)priv;

	ecm_free(ppriv->DOM_ID);
	ecm_free(ppriv->desc);
	ecm_free(ppriv);
}

static void
print_BUTTON(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;
	char	*caption;

	caption = dom->priv;
	fprintf(ecx_info->fp, "<input type=button value=\"%s\" name=%s",
		ecm_safe_str(caption), dom->ID);
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">\n");
}

static void
print_RADIO(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;

	fprintf(ecx_info->fp, "<input type=radio %s", (char *)dom->priv);
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">\n");
}

static void
print_SELECT(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;
	SELECT_priv_t	*ppriv = (SELECT_priv_t *)dom->priv;

	if (ppriv->submit_all)
		fprintf(ecx_info->fp, "<input type=hidden name=RFC value=RFC_select_all('%s')\n>", dom->ID);
	fprintf(ecx_info->fp, "<select id=%s name=%s style=\"width:140px\"", dom->ID, dom->ID);
	if (ppriv->multiple)
		fprintf(ecx_info->fp, " multiple");

	attr_print(ecx_info, dom->ATTR);
	
	fprintf(ecx_info->fp, ">\n");
	print_children(dom);
	fprintf(ecx_info->fp, "</select>\n");
}

static void
print_OPTION(ecx_dom_t *dom)
{
	OPTION_priv_t	*ppriv_opt;
	ecx_info_t	*ecx_info = dom->ecx_info;

	fprintf(ecx_info->fp, "<option id=%s", dom->ID);
	ppriv_opt = (OPTION_priv_t *)dom->priv;
	if (!ecm_empty_str(ppriv_opt->value))
		fprintf(ecx_info->fp, " value=\"%s\"", ppriv_opt->value);
	attr_print(ecx_info, dom->ATTR);
	if (ppriv_opt->ppriv_sel->selected == dom)
		fprintf(ecx_info->fp, " selected");
	fprintf(ecx_info->fp, ">");
	print_children(dom);
	fprintf(ecx_info->fp, "</option>\n");
}

static void
free_OPTION(ecm_ptr_t priv)
{
	OPTION_priv_t	*ppriv = (OPTION_priv_t *)priv;

	ecm_free(ppriv->value);
	ecm_free(ppriv);
}

static void
print_CHECKBOX(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;

	fprintf(ecx_info->fp, "<input type=checkbox %s", (char *)dom->priv);
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">\n");
}

static void
print_BR(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;

	fprintf(ecx_info->fp, "<br>\n");
}

static void
print_NOBR(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;

	fprintf(ecx_info->fp, "<nobr>");
	print_children(dom);
	fprintf(ecx_info->fp, "</nobr>");
}

static void
print_SPAN(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;

	fprintf(ecx_info->fp, "<span");
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">");
	print_children(dom);
	fprintf(ecx_info->fp, "</span>");
}

static void
print_IFRAME(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;
	char	*frm_name, *second;

	frm_name=ecm_strtok((char *)dom->priv, "?", &second);

	if (second && (ecm_strcmp("0", second) != 0)) {
		fprintf(ecx_info->fp, "<script language='javascript'>\n");
		fprintf(ecx_info->fp, "window.setInterval('%s.location.reload()', eval(%s));",
			frm_name, second);
		fprintf(ecx_info->fp, "</script>\n");
	}
	fprintf(ecx_info->fp, "<iframe name=%s", frm_name);
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">\n");
	fprintf(ecx_info->fp, "</iframe>\n");
}

static void
print_HIDDEN(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;
	char	*value = dom->priv;

	fprintf(ecx_info->fp, "<input type=hidden name=%s id=%s", dom->ID, dom->ID);
	if (value)
		fprintf(ecx_info->fp, " value=\"%s\">\n", value);
	else
		fprintf(ecx_info->fp, ">\n");
}

static void
print_ANCHOR(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;

	fprintf(ecx_info->fp, "<a id=%s", dom->ID);

	if (ecm_empty_str(dom_get_attr(dom, "href")))
		dom_del_attr(dom, "href");

	attr_print(ecx_info, dom->ATTR);

	fprintf(ecx_info->fp, ">");
	print_children(dom);
	fprintf(ecx_info->fp, "</a>");
}

static void
print_IMG(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;

	fprintf(ecx_info->fp, "<img name=%s id=%s", dom->ID, dom->ID);
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">");
}

static void
print_TEXT(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;

	fprintf(ecx_info->fp, "<span id=%s", dom->ID);
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">%s</span>\n", ecm_safe_str((char *)dom->priv));
}

static void
print_TABLE(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;

	fprintf(ecx_info->fp, "<table id=%s", dom->ID);
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">\n");
	print_children(dom);
	fprintf(ecx_info->fp, "</table>\n");
}

static void
print_TR(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;

	fprintf(ecx_info->fp, "<tr id=%s", dom->ID);
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">");
	print_children(dom);
	fprintf(ecx_info->fp, "</tr>\n");
}

static void
print_TD(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;

	fprintf(ecx_info->fp, "<td id=%s", dom->ID);
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">");
	print_children(dom);
	fprintf(ecx_info->fp, "</td>\n");
}

static void
print_DIV(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;

	fprintf(ecx_info->fp, "<div name=%s id=%s", dom->ID, dom->ID);
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">");
	print_children(dom);
	fprintf(ecx_info->fp, "</div>\n");
}

static void
print_UPLOAD(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;
	char		*empty_desc;

	if (is_MSIE(ecx_info)) {
		fprintf(ecx_info->fp, "<object codebase=/UploadCtl.ocx#version=1,0,0,1 ");
		fprintf(ecx_info->fp, "id=X%s width=0 height=0 ", dom->ID);
		fprintf(ecx_info->fp, "classid=CLSID:63432545-6F7E-429A-BB03-38D7BCBB2D1F\n></object>");
		fprintf(ecx_info->fp, "<input type=hidden name=PREUPLOAD value=%s>", dom->ID);
	}

	fprintf(ecx_info->fp, "<input type=file name=%s id=%s", dom->ID, dom->ID);
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">");

	empty_desc = dom->priv;
	fprintf(ecx_info->fp, "<input type=hidden name=RFC value=");
	if (!ecm_empty_str(empty_desc))
		fprintf(ecx_info->fp, "\"RFC_UPLOAD_nonempty('%s','%s')\">\n",
			dom->ID, empty_desc);
	else
		fprintf(ecx_info->fp, "\"RFC_UPLOAD_emptyok('%s')\">\n", dom->ID);
}

static void
print_FILE_BROWSE(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;
	char		*empty_desc;

	fprintf(ecx_info->fp, "<input type=hidden name=%s id=%s>", dom->ID, dom->ID);
	fprintf(ecx_info->fp, "<input type=file name=FILEBROWSE id=X%s ", dom->ID);
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">");

	empty_desc = dom->priv;
	fprintf(ecx_info->fp, "<input type=hidden name=RFC value=");
	fprintf(ecx_info->fp, "\"RFC_FILE_BROWSE('%s','%s')\">\n", dom->ID, ecm_safe_str(empty_desc));
}

static void
print_OBJECT(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;
	char	*codebase;
	OBJECT_priv_t	*ppriv;

	ppriv = (OBJECT_priv_t *)dom->priv;
	if (ecm_empty_str(ppriv->version))
		codebase = ecm_strdup(eds_get_embed(ppriv->embid));
	else
		ecm_asprintf(&codebase, "%s#version=%s", eds_get_embed(ppriv->embid), ppriv->version);
	fprintf(ecx_info->fp, "<object codebase=%s name=%s id=%s", codebase, dom->ID, dom->ID);
	ecm_free(codebase);
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">");
	print_children(dom);
	fprintf(ecx_info->fp, "\n</object>\n");
}

static void
free_OBJECT(ecm_ptr_t priv)
{
	OBJECT_priv_t	*ppriv = (OBJECT_priv_t *)priv;

	ecm_free(ppriv->embid);
	ecm_free(ppriv->version);
	ecm_free(ppriv);
}

static void
print_PARAM(ecx_dom_t *dom)
{
	ecx_info_t	*ecx_info = dom->ecx_info;
	PARAM_priv_t	*ppriv;

	ppriv = (PARAM_priv_t *)dom->priv;
	
	fprintf(ecx_info->fp, "<param name=%s value=\"%s\"", ppriv->name, ppriv->value);
	attr_print(ecx_info, dom->ATTR);
	fprintf(ecx_info->fp, ">\n");
}

static void
free_PARAM(ecm_ptr_t priv)
{
	PARAM_priv_t	*ppriv = (PARAM_priv_t *)priv;

	ecm_free(ppriv->name);
	ecm_free(ppriv->value);
	ecm_free(ppriv);
}

static domspec_t	domspecs[] = {
	{ ECM_FALSE, ECM_FALSE, ECM_FALSE, ECM_FALSE,
	  0, print_UNKNOWN, NULL, "UNKNOWN" },
	{ ECM_TRUE, ECM_FALSE, ECM_FALSE, ECM_FALSE,
	  0, print_FORM, NULL, "FORM" },
	{ ECM_TRUE, ECM_TRUE, ECM_FALSE, ECM_FALSE,
	  0, NULL, NULL, "NOFORM" },
	{ ECM_TRUE, ECM_TRUE, ECM_FALSE, ECM_FALSE,
	  0, NULL, NULL, "DYNAMIC" },
	{ ECM_TRUE, ECM_TRUE, ECM_TRUE, ECM_FALSE,
	  0, print_GROUP, free_GROUP, "GROUP" },
	{ ECM_FALSE, ECM_FALSE, ECM_TRUE, ECM_FALSE,
	  0, print_FIELD, free_FIELD, "FIELD" },
	{ ECM_FALSE, ECM_FALSE, ECM_TRUE, ECM_FALSE,
	  0, print_TEXTAREA, NULL, "TEXTAREA" },
	{ ECM_FALSE, ECM_FALSE, ECM_TRUE, ECM_FALSE,
	  0, print_PASSWORD, free_PASSWORD, "PASSWORD" },
	{ ECM_FALSE, ECM_FALSE, ECM_FALSE, ECM_FALSE,
	  0, print_BUTTON, NULL, "BUTTON" },
	{ ECM_FALSE, ECM_FALSE, ECM_TRUE, ECM_FALSE,
	  0, print_RADIO, NULL, "RADIO" },
	{ ECM_TRUE, ECM_FALSE, ECM_TRUE, ECM_FALSE,
	  0, print_SELECT, NULL, "SELECT" },
	{ ECM_TRUE, ECM_FALSE, ECM_TRUE, ECM_FALSE,
	  DOM_SELECT, print_OPTION, free_OPTION, "OPTION" },
	{ ECM_FALSE, ECM_FALSE, ECM_TRUE, ECM_FALSE,
	  0, print_CHECKBOX, NULL, "CHECKBOX" },
	{ ECM_FALSE, ECM_FALSE, ECM_FALSE, ECM_FALSE,
	  0, print_BR, NULL, "BR" },
	{ ECM_TRUE, ECM_TRUE, ECM_FALSE, ECM_FALSE,
	  0, print_NOBR, NULL, "NOBR" },
	{ ECM_TRUE, ECM_TRUE, ECM_FALSE, ECM_FALSE,
	  0, print_SPAN, NULL, "SPAN" },
	{ ECM_FALSE, ECM_FALSE, ECM_FALSE, ECM_FALSE,
	  0, print_IFRAME, NULL, "IFRAME" },
	{ ECM_FALSE, ECM_FALSE, ECM_TRUE, ECM_FALSE,
	  0, print_HIDDEN, NULL, "HIDDEN" },
	{ ECM_TRUE, ECM_FALSE, ECM_FALSE, ECM_TRUE,
	  0, print_ANCHOR, NULL, "A" },
	{ ECM_FALSE, ECM_FALSE, ECM_FALSE, ECM_TRUE,
	  0, print_IMG, NULL, "IMG" },
	{ ECM_FALSE, ECM_FALSE, ECM_FALSE, ECM_TRUE,
	  0, print_TEXT, NULL, "SPAN" },
	{ ECM_TRUE, ECM_TRUE, ECM_FALSE, ECM_FALSE,
	  0, print_TABLE, NULL, "TABLE" },
	{ ECM_TRUE, ECM_FALSE, ECM_FALSE, ECM_FALSE,
	  DOM_TABLE, print_TR, NULL, "TR" },
	{ ECM_TRUE, ECM_FALSE, ECM_FALSE, ECM_TRUE,
	  DOM_TR, print_TD, NULL, "TD" },
	{ ECM_TRUE, ECM_TRUE, ECM_FALSE, ECM_FALSE,
	  0, print_DIV, NULL, "DIV" },
	{ ECM_FALSE, ECM_FALSE, ECM_TRUE, ECM_FALSE,
	  0, print_UPLOAD, NULL, "UPLOAD" },
	{ ECM_FALSE, ECM_FALSE, ECM_TRUE, ECM_FALSE,
	  0, print_FILE_BROWSE, NULL, "FILE_BROWSE" },
	{ ECM_TRUE, ECM_FALSE, ECM_FALSE, ECM_FALSE,
	  0, print_OBJECT, free_OBJECT, "OBJECT" },
	{ ECM_FALSE, ECM_FALSE, ECM_FALSE, ECM_FALSE,
	  DOM_OBJECT, print_PARAM, free_PARAM, "PARAM" },
};

static ecx_dom_t *
get_parent_dom(ecx_info_t *pinfo, ecx_domtype_t type)
{
	DOMinfo_t	*dominfo;
	ecx_dom_t	*parent;

	if (pinfo == NULL)
		return NULL;
	dominfo = (DOMinfo_t *)pinfo->DOMS;
	for (parent = dominfo->dom_cur; parent; parent = parent->parent) {
		if (parent->type == type)
			return parent;
	}
	return NULL;
}

static ecx_dom_t *
find_parent(DOMinfo_t *dominfo, ecx_domtype_t type)
{
	ecx_dom_t	*parent;
	ECMBOOL		recursive;

	recursive = domspecs[type].recursive;
	parent = dominfo->dom_cur;
	while (parent) {
		if (parent->type == type && recursive)
			return parent;
		if (parent->type != type &&
		    (domspecs[type].upper_dom == 0 ||
		     domspecs[type].upper_dom == parent->type))
			return parent;
		parent = parent->parent;
	}
	return parent;
}

static void
print_dom(ecx_dom_t *dom)
{
	DOMinfo_t	*dominfo;
	domspec_t	*domspec = &domspecs[dom->type];

	dominfo = (DOMinfo_t *)dom->ecx_info->DOMS;
	if (!ecm_empty_str(dom->name))
		sr_setc(dominfo->sr_dom_names, dom->name, sr_create_str(ST_STR, dom->ID));
	if (domspec->print)
		domspec->print(dom);
	else
		print_children(dom);
}

static void
print_children(ecx_dom_t *dom)
{
	ecx_dom_t	*child;
	unsigned	i;
	
	for (i = 0; i < dom->childs->len; i++) {
		child = dom->childs->pdata[i];
		if (child == NULL)
			continue;
		print_dom(child);
	}
}

static ECMBOOL
validate_dom_struct(DOMinfo_t *dominfo, ecx_domtype_t type)
{
	ecx_dom_t	*parent;
	ECMBOOL		recursive;
	ecx_domtype_t	upper_type;

	recursive = domspecs[type].recursive;
	upper_type = domspecs[type].upper_dom;
	if (recursive && upper_type == 0)
		return ECM_TRUE;
	parent = dominfo->dom_cur;
	while (parent) {
		if (parent->type == upper_type)
			return ECM_TRUE;
		if (parent->type == type && !recursive && upper_type == 0)
			return ECM_FALSE;
		parent = parent->parent;
	}
	if (upper_type == 0)
		return ECM_TRUE;
	return ECM_FALSE;
}

static ecx_dom_t *
create_dom(ecx_t *ecx, ecx_domtype_t type, const char *name)
{
	DOMinfo_t	*dominfo = (DOMinfo_t *)ecx->info->DOMS;
	domspec_t	*domspec;
	ecx_dom_t	*parent;
	ecx_dom_t	*dom;

	if (!validate_dom_struct(dominfo, type))
		return NULL;

	dom = ecm_malloc(sizeof(ecx_dom_t));
	dom->ecx_info = ecx->info;
	dom->type = type;
	domspec = &domspecs[type];

	ecm_asprintf(&dom->ID, "DOM%d", dominfo->dom_seq++);
	dom->ATTR = NULL;
	dom->priv = NULL;
	dom->STAGE = ecm_strdup(ecx->STAGE);
	dom->name = ecm_strdup(name);
	dom->childs = ecm_ptrs_new(0);

	parent = find_parent(dominfo, type);
	if (parent) {
		dom->parent = parent;
		ecm_ptrs_add(parent->childs, dom);
	}
	else {
		ecm_ptrs_add(dominfo->dom_roots, dom);
		dom->parent = NULL;
	}
	if (domspec->block_level)
		dominfo->dom_cur = dom;

	ecm_ptrs_add(dominfo->doms, dom);
	return dom;
}

static void
free_dom(ecx_dom_t *dom)
{
	unsigned	i;

	if (dom == NULL)
		return;
	for (i = 0; i < dom->childs->len; i++)
		free_dom(dom->childs->pdata[i]);
	if (dom->parent)
		ecm_ptrs_remove_data(dom->parent->childs, dom);
	ecm_ptrs_free(dom->childs);
	ecm_free(dom->ID);
	ecm_free(dom->STAGE);
	ecm_free(dom->name);
	if (dom->priv) {
		domspec_t	*domspec = &domspecs[dom->type];

		if (domspec->free)
			domspec->free(dom->priv);
		else
			ecm_free(dom->priv);
	}
	ecm_free(dom);
}

static ecx_dom_t *
pop_current_dom(ecx_info_t *ecx_info, ecx_domtype_t type)
{
	DOMinfo_t	*dominfo = ecx_info->DOMS;
	ecx_dom_t	*dom_cur;

	dom_cur = dominfo->dom_cur;
	while (dom_cur) {
		if (dom_cur->type == type) {
			dominfo->dom_cur = dom_cur->parent;
			return dom_cur;
		}

		if (domspecs[dom_cur->type].recursive)
			break;
		dom_cur = dom_cur->parent;
	}
	push_error(ecx_info, -ERR_ECX_WRONGSTRUCT, "no matching %s", domspecs[type].tagname);
	return NULL;
}

void
ecx_create_DOM(ecx_info_t *ecx_info)
{
	DOMinfo_t	*dominfo;

	dominfo = ecm_malloc(sizeof(DOMinfo_t));
	dominfo->doms = ecm_ptrs_new(0);
	dominfo->dom_roots = ecm_ptrs_new(0);
	dominfo->dom_cur = NULL;
	dominfo->dom_seq = 0;
	dominfo->sr_dom_names = NULL;
	ecx_info->DOMS = dominfo;
}

static void
free_all_doms(ecm_ptrs_t *dom_roots)
{
	ecx_dom_t	*dom;
	unsigned	i;

	for (i = 0; i < dom_roots->len; i++) {
		dom = dom_roots->pdata[i];
		if (dom == NULL)
			continue;
		free_dom(dom);
	}
}

void
ecx_recreate_DOM(ecx_info_t *ecx_info)
{
	DOMinfo_t	*dominfo = ecx_info->DOMS;

	free_all_doms(dominfo->dom_roots);
	ecm_ptrs_clear(dominfo->dom_roots, 0);
	ecm_ptrs_clear(dominfo->doms, 0);
	dominfo->dom_cur = NULL;
	dominfo->dom_seq = 0;
	sr_free(dominfo->sr_dom_names);
	dominfo->sr_dom_names = NULL;
}

void
ecx_free_DOM(ecx_info_t *ecx_info)
{
	DOMinfo_t	*dominfo = ecx_info->DOMS;

	free_all_doms(dominfo->dom_roots);

	ecm_ptrs_free(dominfo->dom_roots);
	ecm_ptrs_free(dominfo->doms);
	sr_free(dominfo->sr_dom_names);
	ecm_free(dominfo);
}

sr_t *
ecx_print_DOM(ecx_info_t *ecx_info)
{
	DOMinfo_t	*dominfo = ecx_info->DOMS;
	ecm_ptrs_t	*dom_roots, *doms;
	sr_t		*sr_dom_names;
	ecx_dom_t	*dom;
	unsigned	i;

	dominfo->sr_dom_names = sr_create_dir();
	dom_roots = dominfo->dom_roots;
	for (i = 0; i < dom_roots->len; i++) {
		dom = dom_roots->pdata[i];
		print_dom(dom);
	}
	doms = dominfo->doms;
	for (i = 0; i < doms->len; i++) {
		char	*escname, *ESTAGE;
		const char	*name_category;

		dom = doms->pdata[i];
		if (dom == NULL || !domspecs[dom->type].applicable)
			continue;

		escname = escape_str(dom->name);
		ESTAGE = escape_str(dom->STAGE);
		if (dom->type == DOM_UPLOAD)
			name_category = "UPLOAD";
		else
			name_category = "APPLICANT";
		fprintf(ecx_info->fp, "<input type=hidden name=%s value=%s_01%s_01%s>\n",
			name_category, ESTAGE, ecm_safe_str(escname), dom->ID);
		ecm_free(escname);
		ecm_free(ESTAGE);
	}
	sr_dom_names = dominfo->sr_dom_names;
	dominfo->sr_dom_names = NULL;
	return sr_dom_names;
}

static void
get_DYNAMICS(sr_t *sr_dynamics, ecx_dom_t *dom, ECMBOOL in_dynamic)
{
	domspec_t	*domspec;
	ecx_dom_t	*child;
	unsigned	i;
	sr_t		*sr_attrs;

	for (i = 0; i < dom->childs->len; i++) {
		child = dom->childs->pdata[i];
		get_DYNAMICS(sr_dynamics, child, dom->type == DOM_DYNAMIC ? ECM_TRUE: in_dynamic);
	}

	if (dom->type == DOM_DYNAMIC)
		return;

	domspec = &domspecs[dom->type];
	if (domspec->dynamic && in_dynamic) {
		sr_attrs = attr_get_sr(dom->ATTR);
		if (sr_attrs) {
			sr_t	*sr_dynamic;

			sr_dynamic = sr_append(sr_dynamics, sr_create_dir());
			sr_set_name(sr_dynamic, dom->ID);
			sr_append(sr_dynamic, sr_create_str(ST_STR, domspec->tagname));
			sr_append(sr_dynamic, sr_attrs);
		}
	}
}

sr_t *
ecx_get_DYNAMICS(ecx_info_t *ecx_info)
{
	DOMinfo_t	*dominfo = ecx_info->DOMS;
	ecm_ptrs_t	*dom_roots;
	ecx_dom_t	*dom;
	sr_t		*sr_dynamics;
	unsigned	i;

	sr_dynamics = sr_create_dir();
	dom_roots = dominfo->dom_roots;
	for (i = 0; i < dom_roots->len; i++) {
		dom = dom_roots->pdata[i];
		get_DYNAMICS(sr_dynamics, dom, ECM_FALSE);
	}
	return sr_dynamics;
}

void
dom_add_attr(ecx_t *ecx, ECXDOM_t DOM, const char *attr, const char *value)
{
	ecx_dom_t	*dom = (ecx_dom_t *)DOM;

	if (dom->ATTR == NULL)
		dom->ATTR = create_attrset(ecx, NULL, NULL);
	add_attr(dom->ATTR, attr, value);
}

const char *
dom_get_attr(ECXDOM_t DOM, const char *attr)
{
	ecx_dom_t	*dom = (ecx_dom_t *)DOM;

	if (dom->ATTR == NULL)
		return NULL;
	return get_attr(dom->ATTR, attr);
}

void
dom_del_attr(ECXDOM_t DOM, const char *attr)
{
	ecx_dom_t	*dom = (ecx_dom_t *)DOM;
	del_attr(dom->ATTR, attr);
}

void
dom_set_attr(ecx_t *ecx, ECXDOM_t DOM, ECXATTR_t ATTR)
{
	ecx_dom_t	*dom = (ecx_dom_t *)DOM;

	free_attrset(ecx, dom->ATTR);
	dom->ATTR = create_attrset(ecx, ATTR, NULL);
}

sr_t *
get_applicant_values(ecx_t *ecx, const char *name)
{
	sr_t	*sr_stage_info;
	sr_t	*sr_applicants;
	sr_t	*sr_applicant;

	sr_stage_info = sr_geti(sr_getc(ecx->info->sr_ecx_state, "stages"), 0);
	sr_applicants = sr_getc(sr_stage_info, "APPLICANTS");
	sr_applicant = sr_getc(sr_applicants, ecx->STAGE);
	if (name == NULL)
		return sr_first(sr_applicant);
	return sr_getc(sr_applicant, name);
}

sr_t *
get_COOKIE(ecx_t *ecx, const char *name)
{
	sr_t	*sr_COOKIES, *sr_cookie;
	sr_t	*sr_ecx_state;

	sr_ecx_state = ecx->info->sr_ecx_state;
	sr_COOKIES = sr_getc(sr_ecx_state, "COOKIES");
	if (sr_COOKIES == NULL)
		sr_COOKIES = sr_createc_dir(sr_ecx_state, "COOKIES");

	sr_cookie = sr_getc(sr_COOKIES, name);
	if (sr_cookie == NULL)
		sr_cookie = sr_createc_str(sr_COOKIES, name, ST_STR, NULL);
	return sr_cookie;
}

static const char *
get_applicant_value(ecx_t *ecx, const char *name)
{
	sr_t	*sr_values;

	sr_values = get_applicant_values(ecx, name);
	return sr_geti_str(sr_values, 0);
}

static void
set_applicant_value(ecx_t *ecx, const char *name, const char *value)
{
	sr_t	*sr_stage_info;
	sr_t	*sr_applicants;
	sr_t	*sr_applicant, *sr_values;

	sr_stage_info = sr_geti(sr_getc(ecx->info->sr_ecx_state, "stages"), 0);
	sr_applicants = sr_getc(sr_stage_info, "APPLICANTS");
	if (sr_applicants == NULL)
		sr_applicants = sr_addc(sr_stage_info, "APPLICANTS", sr_create_dir());
	sr_applicant = sr_getc(sr_applicants, ecx->STAGE);
	if (sr_applicant == NULL)
		sr_applicant = sr_addc(sr_applicants, ecx->STAGE, sr_create_dir());
	if (ecm_empty_str(name)) {
		sr_values = sr_first(sr_applicant);
		if (sr_values == NULL)
			sr_values = sr_append(sr_applicant, sr_create_dir());
	}
	else {
		sr_values = sr_getc(sr_applicant, name);
		if (sr_values == NULL)
			sr_values = sr_append(sr_applicant, sr_set_name(sr_create_dir(), name));
	}
	sr_clear_child(sr_values);
	sr_append(sr_values, sr_create_str(ST_STR, value));
}

const char *
DOM_ID(ecx_t *ecx, ECXDOM_t DOM)
{
	ecx_dom_t	*dom = (ecx_dom_t *)DOM;
	return dom->ID;
}

ECMBOOL
dom_ecx_has_form(ecx_t *ecx)
{
	ecx_info_t	*ecx_info = ecx->info;
	DOMinfo_t	*dominfo = ecx_info->DOMS;
	ecx_dom_t	*dom_cur;

	dom_cur = dominfo->dom_cur;
	while (dom_cur) {
		if (dom_cur->type == DOM_NOFORM)
			return ECM_FALSE;
		if (dom_cur->type == DOM_FORM)
			return ECM_TRUE;
		dom_cur = dom_cur->parent;
	}
	return ECM_FALSE;
}

static ecm_size_t
get_htmltext_size(const char *str)
{
	const char	*p = str;
	ecm_size_t	size = 1;

	while (*p) {
		switch (*p) {
		case ' ':
			if (p[1] == ' ' || p[1] == '\0')
				size += 6;
			else
				size++;
			break;
		case '<':
		case '>':
			size += 4;
			break;
		case '&':
			size += 5;
			break;
		case '\'':
		case 34:	/* quotation mark */
			size += 6;
			break;
		default:
			size++;
			break;
		}
		p++;
	}
	return size;
}

static char *
get_htmltext(const char *str)
{
	const char	*p = str;
	char	*buf;
	int	n;

	if (ecm_empty_str(str))
		return ecm_strdup("");
	buf = ecm_malloc(get_htmltext_size(str));
	n = 0;
	while (*p) {
		switch (*p) {
		case ' ':
			if (p[1] == ' ' || p[1] == '\0')
				n += ecm_sprintf(buf + n, "&nbsp;");
			else
				buf[n++] = ' ';
			break;
		case '<':
			n += ecm_sprintf(buf + n, "&lt;");
			break;
		case '>':
			n += ecm_sprintf(buf + n, "&gt;");
			break;
		case '&':
			n += ecm_sprintf(buf + n, "&amp;");
			break;
		case '\'':
		case 34:	/* quotation mark */
			n += ecm_sprintf(buf + n, "&quot;");
			break;
		default:
			buf[n++] = *p;
			break;
		}
		p++;
	}
	buf[n] = '\0';
	return buf;
}

ECXDOM_t
UNKNOWN(ecx_t *ecx, const char *buf, ECMBOOL is_raw)
{
	ecx_dom_t	*dom;

	dom = create_dom(ecx, DOM_UNKNOWN, NULL);
	if (dom && ecx->state != ECX_UPDATING) {
		if (is_raw)
			dom->priv = ecm_strdup(buf);
		else
			dom->priv = get_htmltext(buf);
	}

	return dom;
}

ECXDOM_t
FORM_BEGIN(ecx_t *ecx)
{
	ecx_dom_t	*dom;

	if (ecx_is_applying())
		return NULL;

	if (ecx_is_editing()) {
		push_error(ecx->info, -ERR_ECX_WRONGSTRUCT, "multiple <form>");
		return NULL;
	}
	dom = create_dom(ecx, DOM_FORM, NULL);
	if (dom && ecx->state != ECX_UPDATING) {
		char	*asrv_STAGE, *ESTAGE;

		sr_asrv_asprint(&asrv_STAGE, ST_STAGE, sr_get_val(ecx->sr_stage));
		ESTAGE = escape_str(asrv_STAGE);
		ecm_free(asrv_STAGE);
		ecm_asprintf((char **)&dom->priv, "%s_01%s", dom->ID, ESTAGE);
		ecm_free(ESTAGE);
		return dom;
	}
	return NULL;
}

void
FORM_END(ecx_t *ecx)
{
	if (ecx_is_applying())
		return;
	pop_current_dom(ecx->info, DOM_FORM);
}

void
FORM_BUTTON(ecx_t *ecx, const char *embed_id, ecx_formbutton_t type)
{
	ecx_dom_t	*form;
	char	*href;

	if (ecx == NULL)
		return;

	if (!ecx_is_editing())
		return;

	form = get_parent_dom(ecx->info, DOM_FORM);
	if (form == NULL) {
		push_error(ecx->info, -ERR_ECX_WRONGSTRUCT, "form button is not in <form>");
		return;
	}

	switch (type) {
	case ECX_FORM_APPLY:
		ecm_asprintf(&href, "javascript:form_apply('%s')", form->ID);
		break;
	case ECX_FORM_APPLY_COMMIT:
		ecm_asprintf(&href, "javascript:form_apply('%s',true)", form->ID);
		break;
	case ECX_FORM_CANCEL:
		ecm_asprintf(&href, "javascript:form_cancel('%s')", form->ID);
		break;
	default:
		return;
	}

	ANCHOR_BEGIN(ecx, NULL, href);
	ecm_free(href);
	IMG(ecx, NULL, embed_id, NULL);
	ANCHOR_END(ecx);
}

/* TODO: */
#if 0
static ECMBOOL
relocate_stage(ecx_t *ecx, sr_t *sr_anchor, sr_t *sr_stage)
{
	sr_t	*sr_LNPs;
	char	*LNP;

	LNP = ecxroot_get_LNP(ecx->info->conn->ecxroot, sr_anchor);
	if (LNP == NULL)
		return ECM_FALSE;

	sr_LNPs = sr_getc(sr_stage, "LNP");
	if (sr_LNPs == NULL)
		sr_LNPs = sr_addc(sr_stage, "LNP", sr_create_dir());
	sr_add_first(sr_LNPs, get_sr_LNP(LNP));
	ecm_free(LNP);
	return ECM_TRUE;
}
#endif

char *
mangle_escaped_stage(ecx_t *ecx, const sr_t *sr_cstage)
{
	sr_t	*sr_stage;
	char	*ESTAGE, *asrv_stage;

	sr_stage = sr_dup(sr_cstage);
	mangle_stage(ecx->sr_stage, sr_stage);

	if (sr_asrv_asprint(&asrv_stage, ST_STAGE, sr_get_val(sr_stage)) < 0) {
		sr_free(sr_stage);
		return NULL;
	}
	sr_free(sr_stage);
	ESTAGE = escape_str(asrv_stage);
	ecm_free(asrv_stage);
	return ESTAGE;
}

char *
mangle_escaped_arg(ecx_t *ecx, const sr_t *sr_arg)
{
	char	*ARG, *EARG;

	sr_asr_asprint(&ARG, sr_arg);
	EARG = escape_str(ARG);
	ecm_free(ARG);
	return EARG;
}

void
ACTION_BUTTON(ecx_t *ecx, const char *name, const char *embed_id, ECXDOM_t DOM, const char *value)
{
	char		*href = NULL;

	if (ecx == NULL)
		return;

	if (!ecm_empty_str(value)) {
		ecx_dom_t	*hidden;
		char		*ESTAGE, *escvalue;

		hidden = DOM;
		escvalue = escape_str(value);
		ESTAGE = escape_str(ecx->STAGE);
		ecm_asprintf(&href, "javascript:form_action_button('%s','%s','%s')",
			     ESTAGE, hidden->ID, ecm_safe_str(escvalue));
		ecm_free(escvalue);
		ecm_free(ESTAGE);
	}

	ANCHOR_BEGIN(ecx, name, href);
	IMG(ecx, name, embed_id, NULL);
	ecm_free(href);
	ANCHOR_END(ecx);
}

void
edbd_ecx_NOFORM_BEGIN(ecx_t *ecx)
{
	create_dom(ecx, DOM_NOFORM, NULL);
}

void
edbd_ecx_NOFORM_END(ecx_t *ecx)
{
	pop_current_dom(ecx->info, DOM_NOFORM);
}

static void
clear_stage(ecx_info_t *ecx_info)
{
	sr_t	*sr_ecx_state;
	sr_t	*sr_stage_infos;

	sr_ecx_state = ecx_info->sr_ecx_state;
	sr_stage_infos = sr_getc(sr_ecx_state, "stages");
	sr_clear_child(sr_stage_infos);
}

static void
add_stage(ecx_info_t *ecx_info, sr_t *sr_stage)
{
	sr_t	*sr_ecx_state;
	sr_t	*sr_stage_infos;
	sr_t	*sr_stage_info;

	sr_ecx_state = ecx_info->sr_ecx_state;
	sr_stage_infos = sr_getc(sr_ecx_state, "stages");

	sr_stage_info = sr_create_dir();
	sr_add_first(sr_stage_infos, sr_stage_info);
	sr_addc(sr_stage_info, "stage", sr_stage);
}

static ECMBOOL
pop_stage(ecx_info_t *ecx_info, ecm_uint32 nsteps)
{
	sr_t	*sr_ecx_state;
	sr_t	*sr_stage_infos;
	ecm_uint32	i;

	sr_ecx_state = ecx_info->sr_ecx_state;
	sr_stage_infos = sr_getc(sr_ecx_state, "stages");
	if (sr_get_num_child(sr_stage_infos) < (int)nsteps)
		return ECM_FALSE;
	for (i = 0; i < nsteps; i++)
		sr_free_first(sr_stage_infos);
	return ECM_TRUE;
}

void
edbd_ecx_SANCHOR(ecx_t *ecx, ecx_sanchor_mode_t mode,
		 const sr_t *sr_anchor, const sr_t *csr_stage)
{
	ecx_info_t	*ecx_info = ecx->info;
	sr_t		*sr_stage = NULL, *sr_STAGE = NULL;

	if (ecx_info->stagetouch != ECX_STAGETOUCH_NONE) {
		push_error(ecx->info, -ERR_ECX_WRONGSTRUCT, "already redirected or linked");
		return;
	}

	if (sr_anchor) {
		csr_stage = sr_stage = get_anchored_stage(ecx_info->conn, csr_stage, sr_anchor);
	}
	if (csr_stage) {
		char	*STAGE;

		if (sr_asrv_asprint(&STAGE, ST_STAGE, sr_get_val(csr_stage)) > 0)
			sr_STAGE = sr_create_str(ST_STR, STAGE);
	}
	sr_free(sr_stage);

	switch (mode) {
	case ECX_SANCHOR_LINK:
		add_stage(ecx_info, sr_STAGE);
		ecx_info->stagetouch = ECX_STAGETOUCH_LINKED;
		break;
	case ECX_SANCHOR_GO:
	case ECX_SANCHOR_ABORT:
		clear_stage(ecx_info);
		add_stage(ecx_info, sr_STAGE);
		if (mode == ECX_SANCHOR_GO)
			ecx_info->need_ecxroot_commit = ECM_TRUE;
		ecx_info->stagetouch = ECX_STAGETOUCH_REDIRECTED;
		break;
	case ECX_SANCHOR_ACTION:
		add_stage(ecx_info, sr_STAGE);
		ecx_info->need_ecxroot_commit = ECM_TRUE;
		break;
	}
}

ECMBOOL
edbd_ecx_BACK(ecx_t *ecx, ECMBOOL abortive, ecm_uint32 nsteps)
{
	ecx_info_t	*ecx_info = ecx->info;
	ECMBOOL		res;

	if (ecx_info->stagetouch != ECX_STAGETOUCH_NONE)
		return ECM_FALSE;
	res = pop_stage(ecx_info, nsteps);
	if (res) {
		if (!abortive)
			ecx_info->need_ecxroot_commit = ECM_TRUE;
		ecx_info->stagetouch = ECX_STAGETOUCH_REDIRECTED;
	}
	return res;
}

ECXDOM_t
SANCHOR_BEGIN(ecx_t *ecx, ecx_sanchor_mode_t mode, const char *name,
	      const sr_t *sr_stage, const char *confirm_msg)
{
	ECXDOM_t	DOM;
	char	*href = NULL;
	char	*ESTAGE, *htmlsafe_msg;

	if (ecx_is_applying())
		return NULL;

	htmlsafe_msg = get_htmltext(confirm_msg);

	ESTAGE = mangle_escaped_stage(ecx, sr_stage);
	switch (mode) {
	case ECX_SANCHOR_LINK:
		ecm_asprintf(&href, "javascript:form_link('%s',null,'%s')",
			     ecm_safe_str(ESTAGE), htmlsafe_msg);
		break;
	case ECX_SANCHOR_GO:
		ecm_asprintf(&href, "javascript:stage_go('%s','%s')",
			     ecm_safe_str(ESTAGE), htmlsafe_msg);
		break;
	case ECX_SANCHOR_ACTION:
		ecm_asprintf(&href, "javascript:form_action_stage('%s',null,'%s')",
			     ecm_safe_str(ESTAGE), htmlsafe_msg);
	default:
		break;
	}
	
	ecm_free(htmlsafe_msg);
	ecm_free(ESTAGE);

	DOM = ANCHOR_BEGIN(ecx, name, href);
	ecm_free(href);
	return DOM;
}

void
SANCHOR_END(ecx_t *ecx)
{
	if (ecx_is_applying())
		return;

	ANCHOR_END(ecx);
}

ECXDOM_t
edbd_ecx_TEXT(ecx_t *ecx, const char *name, const char *text)
{
	ecx_dom_t	*dom;

	dom = create_dom(ecx, DOM_TEXT, name);
	if (dom) {
		char	*htmltext = get_htmltext(text);
		dom_add_attr(ecx, dom, "innerHTML", htmltext);
		if (ecx->state != ECX_UPDATING)
			dom->priv = htmltext;
		else
			ecm_free(htmltext);
	}
	return dom;
}

void
POPUP_BEGIN(ecx_t *ecx, sr_t *sr_stage, const char *name, int wt, int ht)
{
	char	*href = NULL;
	char	*ESTAGE;

	if (ecx_is_applying())
		return;

	if ((ESTAGE = mangle_escaped_stage(ecx, sr_stage))) {
		ecm_asprintf(&href, "javascript:pop_window('%s', '%s', %d, %d)",
		     ecm_safe_str(ESTAGE),
		     ecm_safe_str(name),
		     wt, ht);
		ecm_free(ESTAGE);
	}

	ANCHOR_BEGIN(ecx, NULL, href);
	ecm_free(href);
}

void
POPUP_END(ecx_t *ecx)
{
	ANCHOR_END(ecx);
}

void
POPUP_FORM_BUTTON(ecx_t *ecx, const char *embed_id, ecx_formbutton_t type, char *action)
{
	ecx_dom_t	*form;
	char	*href;

	if (ecx == NULL)
		return;

	if (!ecx_is_editing())
		return;

	form = get_parent_dom(ecx->info, DOM_FORM);
	if (form == NULL) {
		push_error(ecx->info, -ERR_ECX_WRONGSTRUCT, "form button is not in <form>");
		return;
	}

	switch (type) {
	case ECX_FORM_APPLY:
		ecm_asprintf(&href, "javascript:popup_form_apply('%s', '%s')", 
			     form->ID, ecm_safe_str(action));
		break;
	case ECX_FORM_CANCEL:
		ecm_asprintf(&href, "javascript:popup_form_cancel('%s')", form->ID);
		break;
	default:
		return;
	}

	ANCHOR_BEGIN(ecx, NULL, href);
	ecm_free(href);
	IMG(ecx, NULL, embed_id, NULL);
	ANCHOR_END(ecx);
}

ECXDOM_t
ANCHOR_BEGIN(ecx_t *ecx, const char *name, const char *href)
{
	ecx_dom_t	*dom;

	if ((dom = create_dom(ecx, DOM_ANCHOR, name)) == NULL)
		return NULL;
	dom_add_attr(ecx, dom, "href", href);
	return dom;
}

void
ANCHOR_END(ecx_t *ecx)
{
	pop_current_dom(ecx->info, DOM_ANCHOR);
}

void
DYNAMIC_BEGIN(ecx_t *ecx, ECMBOOL enabled)
{
	DOMinfo_t	*dominfo;
	ecx_dom_t	*dom;

	dominfo = (DOMinfo_t *)ecx->info->DOMS;
	dom = create_dom(ecx, DOM_DYNAMIC, NULL);
	if (dom) {
		DYNAMIC_priv_t	*priv;

		dom->priv = priv = ecm_malloc(sizeof(DYNAMIC_priv_t));
		priv->enabled = enabled;
	}
	if (!enabled)
		ecx->no_dynamic = ECM_TRUE;
}

void
DYNAMIC_END(ecx_t *ecx)
{
	pop_current_dom(ecx->info, DOM_DYNAMIC);
}

ECXDOM_t
IMG(ecx_t *ecx, const char *name, const char *embed_id, const sr_t *sr_stage)
{
	ecx_dom_t	*img;
	char		*path = NULL;

	if (ecx_is_applying())
		return NULL;
	if (embed_id) {
		path = ecm_strdup(eds_get_embed(embed_id));
		if (path == NULL) {
			push_error(ecx->info, -ERR_ECX_WRONGSTRUCT,
				   "%s: embed id is invalid", ecm_safe_str(embed_id));
			return NULL;
		}
	}
	else {
		char	*ESTAGE;

		if ((ESTAGE = mangle_escaped_stage(ecx, sr_dup(sr_stage))))
			ecm_asprintf(&path, "/ecx?GO_STAGE=%s", ESTAGE);
		ecm_free(ESTAGE);
	}
	if ((img = create_dom(ecx, DOM_IMG, name)) == NULL)
		return NULL;
	dom_add_attr(ecx, img, "src", path);
	ecm_free(path);
	return img;
}

static ECM_INLINE const char *
getv_INPUT(ecx_t *ecx, const char *fmt, va_list ap)
{
	char		*name = NULL;
	const char	*value;

	if (fmt)
		ecm_vasprintf(&name, fmt, ap);

	value = get_applicant_value(ecx, name);
	ecm_free(name);
	return value;
}

ECXDOM_t
GROUP_BEGIN(ecx_t *ecx, const char *name, const char *empty_desc, const char *empty_selection_desc)
{
	ecx_dom_t	*dom;

	dom = create_dom(ecx, DOM_GROUP, name);
	if (dom && ecx->state != ECX_UPDATING) {
		GROUP_priv_t	*priv;

		dom->priv = priv = ecm_malloc(sizeof(GROUP_priv_t));
		priv->empty_desc = ecm_strdup(empty_desc);
		priv->empty_selection_desc = ecm_strdup(empty_selection_desc);
	}
	return dom;
}

void
GROUP_END(ecx_t *ecx)
{
	if (ecx_is_applying())
		return;
	pop_current_dom(ecx->info, DOM_GROUP);
}

ECXDOM_t
edbd_ecx_SELECT_BEGIN(ecx_t *ecx, const char *name, ECMBOOL submit_all, ECMBOOL multiple)
{
	ecx_dom_t	*dom;
	SELECT_priv_t	*ppriv;

	dom = create_dom(ecx, DOM_SELECT, name);
	if (ecx->state == ECX_UPDATING)
		return dom;

	ppriv = ecm_malloc(sizeof(SELECT_priv_t));
	ppriv->submit_all = submit_all;
	ppriv->multiple = multiple;
	ppriv->selected = NULL;
	dom->priv = ppriv;
	return dom;
}

void
edbd_ecx_SELECT_END(ecx_t *ecx)
{
	ecx_dom_t	*dom_sel, *dom_opt;
	SELECT_priv_t	*ppriv;

	if (ecx_is_applying())
		return;
	dom_sel = pop_current_dom(ecx->info, DOM_SELECT);
	if (ecx->state == ECX_UPDATING)
		return;

	ppriv = (SELECT_priv_t *)dom_sel->priv;
	if ((dom_opt = ppriv->selected))
		set_applicant_value(ecx, dom_sel->name, ((OPTION_priv_t *)dom_opt->priv)->value);
}

ECXDOM_t
edbd_ecx_OPTION(ecx_t *ecx, ECMBOOL selected, const char *value)
{
	ecx_dom_t	*dom, *dom_sel;
	SELECT_priv_t	*ppriv_sel;
	OPTION_priv_t	*ppriv_opt;

	dom_sel = get_parent_dom(ecx->info, DOM_SELECT);
	if (dom_sel == NULL)
		return NULL;

	dom = create_dom(ecx, DOM_OPTION, NULL);
	if (ecx->state == ECX_UPDATING)
		return dom;
	ppriv_sel = (SELECT_priv_t *)dom_sel->priv;

	if (selected || ppriv_sel->selected == NULL)
		ppriv_sel->selected = dom;
	dom->priv = ppriv_opt = ecm_malloc(sizeof(OPTION_priv_t));
	ppriv_opt->ppriv_sel = ppriv_sel;
	ppriv_opt->value = ecm_strdup(value);
	return dom;
}

static void
parse_range(FIELD_rule_t *rule, const char *c_range)
{
	char	*range = ecm_strdup(c_range), *max;
	char	*comma;

	if ((comma = strchr(range, ','))) {
		*comma = '\0';
		max = comma + 1;
	}
	else
		max = NULL;

	switch (rule->type) {
	case FRULE_INT: {
		if (ecm_sscanf(range, "%I64d", &rule->range.irange.min) == 1)
			rule->has_min = ECM_TRUE;
		if (ecm_sscanf(max, "%I64d", &rule->range.irange.max) == 1)
			rule->has_max = ECM_TRUE;
		if (rule->has_min && rule->has_max &&
		    rule->range.irange.min > rule->range.irange.max) {
			rule->has_min = ECM_FALSE;
			rule->has_max = ECM_FALSE;
		}
		break;
	}
	case FRULE_FLOAT:
		if (ecm_sscanf(range, "%f", &rule->range.drange.min) == 1)
			rule->has_min = ECM_TRUE;
		if (ecm_sscanf(max, "%f", &rule->range.drange.max) == 1)
			rule->has_max = ECM_TRUE;
		if (rule->has_min && rule->has_max &&
		    rule->range.drange.min > rule->range.drange.max) {
			rule->has_min = ECM_FALSE;
			rule->has_max = ECM_FALSE;
		}
		break;
	default:
		break;
	}
	ecm_free(range);
}

static FIELD_rule_t *
parse_FIELD_rule(const char *rulestr)
{
	char	*p;
	FIELD_rule_t	*rule;

	p = ecm_find_nonblank(rulestr);
	if (*p != '%')
		return NULL;
	rule = (FIELD_rule_t *)ecm_malloc(sizeof(*rule));
	rule->has_min = ECM_FALSE;
	rule->has_max = ECM_FALSE;
	switch (p[1]) {
	case 's':
		rule->type = FRULE_NONEMPTY;
		return rule;
	case 'd':
	case 'f':
		if (p[1] == 'd')
			rule->type = FRULE_INT;
		else
			rule->type = FRULE_FLOAT;
		if (p[2] == ':')
			parse_range(rule, p + 3);
		return rule;
	default:
		return NULL;
	}
}

ECXDOM_t
FIELDv(ecx_t *ecx, const char *name, const char *desc, const char *rulestr, const char *fmt, va_list ap, const sr_t *sr_stage)
{
	ecx_dom_t	*dom;
	FIELD_rule_t	*rule = NULL;
	FIELD_priv_t	*priv;

	if (ecx == NULL)
		return NULL;
	if (!ecm_empty_str(rulestr) &&
	    (rule = parse_FIELD_rule(rulestr)) == NULL) {
		push_error(ecx->info, -ERR_ECX_GENERAL, "invalid FIELD rule: %s", rule);
		return NULL;
	}
	if ((dom = create_dom(ecx, DOM_FIELD, name)) == NULL) {
		ecm_free(rule);
		return NULL;
	}

	if (ecx->state == ECX_UPDATING) {
		ecm_free(rule);
		return dom;
	}
	else {
		priv = ecm_malloc(sizeof(FIELD_priv_t));
		priv->rule = rule;
		priv->desc = ecm_strdup(desc);
		if (fmt != NULL) {
			char	*value, *alloced;

			ecm_vasprintf(&value, fmt, ap);
			escape_dquot(value, &alloced);
			if (alloced == NULL)
				priv->value = value;
			else {
				priv->value = alloced;
				ecm_free(value);
			}
		}
		else
			priv->value = NULL;
		if (sr_stage) {
			char	*ESTAGE;

			ESTAGE = mangle_escaped_stage(ecx, sr_stage);
			ecm_asprintf(&priv->href, "javascript:form_action_stage('%s')", ESTAGE);
			ecm_free(ESTAGE);
		}
		else
			priv->href = NULL;
		dom->priv = priv;
	}
	
	return dom;
}

ECXDOM_t
FIELD(ecx_t *ecx, const char *name, const char *fmt, ...)
{
	va_list	ap;
	ECXDOM_t	RES;

	va_start(ap, fmt);
	RES = FIELDv(ecx, name, NULL, NULL, fmt, ap, NULL);
	va_end(ap);
	return RES;
}

ECXDOM_t
TEXTAREA(ecx_t *ecx, const char *name, const char *msg)
{
	ecx_dom_t	*dom;

	dom = create_dom(ecx, DOM_TEXTAREA, name);
	if (dom && ecx->state != ECX_UPDATING)
		dom->priv = get_htmltext(msg);
	return dom;
}

ECXDOM_t
edbd_ecx_PASSWORD(ecx_t *ecx, const char *name, const char *DOM_ID, const char *mismatch_desc)
{
	ecx_dom_t	*dom;

	dom = create_dom(ecx, DOM_PASSWORD, name);
	if (dom && ecx->state != ECX_UPDATING && !ecm_empty_str(DOM_ID)) {
		PASSWORD_priv_t	*priv;

		priv = ecm_malloc(sizeof(*priv));
		priv->DOM_ID = ecm_strdup(DOM_ID);
		priv->desc = ecm_strdup(mismatch_desc);
		dom->priv = priv;
	}
	return dom;
}

ECXDOM_t
BUTTON(ecx_t *ecx, const char *name, const char *caption, ECXATTR_t ATTR)
{
	ecx_dom_t	*dom;

	if ((dom = create_dom(ecx, DOM_BUTTON, name)) == NULL)
		return NULL;
	dom->ATTR = ATTR;
	if (ecx->state != ECX_UPDATING)
		dom->priv = ecm_strdup(caption);
	return dom;
}

const sr_t *
get_GROUP(ecx_t *ecx, const char *name)
{
	return get_applicant_values(ecx, name);
}

const char *
getv_FIELD(ecx_t *ecx, const char *fmt, va_list ap)
{
	return getv_INPUT(ecx, fmt, ap);
}

const char *
get_FIELD(ecx_t *ecx, const char *name)
{
	return get_applicant_value(ecx, name);
}

const char *
edbd_ecx_get_PASSWORD(ecx_t *ecx, const char *name)
{
	return get_applicant_value(ecx, name);
}

ECXDOM_t
RADIOv(ecx_t *ecx, const char *name, const char *fmt, va_list ap, ECXATTR_t ATTR)
{
	ecx_dom_t	*dom, *dom_grp;
	const char	*ID;

	if ((dom = create_dom(ecx, DOM_RADIO, name)) == NULL)
		return NULL;
	dom->ATTR = ATTR;
	if (ecx->state == ECX_UPDATING)
		return dom;

	dom_grp = get_parent_dom(ecx->info, DOM_GROUP);
	if (dom_grp)
		ID = dom_grp->ID;
	else
		ID = dom->ID;

	if (fmt) {
		const char	*escval;
		char	*value, *alloced;

		ecm_vasprintf(&value, fmt, ap);
		escval = escape_dquot(value, &alloced);

		if (!ecm_empty_str(escval))
			ecm_asprintf((char **)&dom->priv, "name=%s value=\"%s\"", ID, escval);
		else
			ecm_asprintf((char **)&dom->priv, "name=%s", ID);
		ecm_free(value);
		ecm_free(alloced);
	}
	else
		ecm_asprintf((char **)&dom->priv, "name=%s", ID);

	return dom;
}

const char *
getv_RADIO(ecx_t *ecx, const char *fmt, va_list ap)
{
	return getv_INPUT(ecx, fmt, ap);
}

ECXDOM_t
CHECKBOXv(ecx_t *ecx, const char *name, const char *fmt, va_list ap, ECXATTR_t ATTR)
{
	ecx_dom_t	*dom, *dom_grp;
	const char	*ID;

	if ((dom = create_dom(ecx, DOM_CHECKBOX, name)) == NULL)
		return NULL;
	dom->ATTR = ATTR;

	if (ecx->state == ECX_UPDATING)
		return dom;

	dom_grp = get_parent_dom(ecx->info, DOM_GROUP);
	if (dom_grp)
		ID = dom_grp->ID;
	else
		ID = dom->ID;

	if (fmt) {
		const char	*escval;
		char	*value, *alloced;

		ecm_vasprintf(&value, fmt, ap);
		escval = escape_dquot(value, &alloced);
		if (!ecm_empty_str(escval))
			ecm_asprintf((char **)&dom->priv, "name=%s value=%s", ID, escval);
		else
			ecm_asprintf((char **)&dom->priv, "name=%s", ID);
		ecm_free(value);
		ecm_free(alloced);
	}
	else
		ecm_asprintf((char **)&dom->priv, "name=%s", ID);
	return dom;
}

void 
edbd_ecx_BR(ecx_t *ecx)
{
	create_dom(ecx, DOM_BR, NULL);
}

ECXDOM_t
edbd_ecx_NOBR_BEGIN(ecx_t *ecx)
{
	return create_dom(ecx, DOM_NOBR, NULL);
}

void
edbd_ecx_NOBR_END(ecx_t *ecx)
{
	pop_current_dom(ecx->info, DOM_NOBR);
}

ECXDOM_t
edbd_ecx_SPAN_BEGIN(ecx_t *ecx)
{
	return create_dom(ecx, DOM_SPAN, NULL);
}

void
edbd_ecx_SPAN_END(ecx_t *ecx)
{
	pop_current_dom(ecx->info, DOM_SPAN);
}

ECXDOM_t
IFRAME(ecx_t *ecx, const char *name, int second, ECXATTR_t ATTR)
{
	ecx_dom_t	*dom, *dom_grp;
	const char	*ID;

	if ((dom = create_dom(ecx, DOM_IFRAME, name)) == NULL)
		return NULL;
	dom->ATTR = ATTR;

	if (ecx->state == ECX_UPDATING)
		return dom;

	dom_grp = get_parent_dom(ecx->info, DOM_GROUP);
	if (dom_grp)
		ID = dom_grp->ID;
	else
		ID = dom->ID;
	ecm_asprintf((char **)&dom->priv, "%s?%d", ID, second);

	return dom;
}

ECXDOM_t
edbd_ecx_OBJECT_BEGIN(ecx_t *ecx, const char *name, const char *embid, const char *version)
{
	ecx_dom_t	*dom;
	OBJECT_priv_t	*ppriv;

	if ((dom = create_dom(ecx, DOM_OBJECT, name)) == NULL)
		return NULL;

	if (ecx->state == ECX_UPDATING)
		return dom;

	ppriv = ecm_malloc(sizeof(OBJECT_priv_t));
	ppriv->embid = ecm_strdup(embid);
	ppriv->version = ecm_strdup(version);
	dom->priv = ppriv;
	return dom;
}

void
edbd_ecx_OBJECT_END(ecx_t *ecx)
{
	pop_current_dom(ecx->info, DOM_OBJECT);
}

ECXDOM_t
edbd_ecx_PARAM(ecx_t *ecx, const char *name, const char *value)
{
	ecx_dom_t	*dom, *dom_grp;
	PARAM_priv_t	*ppriv;

	dom_grp = get_parent_dom(ecx->info, DOM_OBJECT);
	if (dom_grp == NULL)
		return NULL;

	dom = create_dom(ecx, DOM_PARAM, NULL);
	if (ecx->state == ECX_UPDATING)
		return dom;
	
	ppriv = ecm_malloc(sizeof(PARAM_priv_t));
	ppriv->name = ecm_strdup(name);
	ppriv->value = ecm_strdup(value);
	dom->priv = ppriv;

	return dom;
}

const sr_t *
getv_CHECKBOX(ecx_t *ecx, const char *fmt, va_list ap)
{
	sr_t		*sr_values;
	char		*name = NULL;

	if (fmt)
		ecm_vasprintf(&name, fmt, ap);
	sr_values = get_applicant_values(ecx, name);
	ecm_free(name);
	return sr_values;
}

const sr_t *
get_SELECT(ecx_t *ecx, const char *name)
{
	sr_t		*sr_values;

	sr_values = get_applicant_values(ecx, name);
	return sr_values;
}

ECXDOM_t
HIDDENv(ecx_t *ecx, const char *name, const char *fmt, va_list ap)
{
	ecx_dom_t	*dom;

	if (ecx == NULL)
		return NULL;
	if ((dom = create_dom(ecx, DOM_HIDDEN, name)) == NULL)
		return NULL;
	if (ecx->state == ECX_UPDATING)
		return dom;
	if (fmt != NULL)
		ecm_vasprintf((char **)&dom->priv, fmt, ap);
	return dom;
}

ECXDOM_t
HIDDEN(ecx_t *ecx, const char *name, const char *fmt, ...)
{
	va_list	ap;
	ECXDOM_t	RES;

	va_start(ap, fmt);
	RES = HIDDENv(ecx, name, fmt, ap);
	va_end(ap);
	return RES;
}

ECXDOM_t
UPLOAD(ecx_t *ecx, const char *name, const char *empty_desc)
{
	ecx_dom_t	*dom;

	dom = create_dom(ecx, DOM_UPLOAD, name);
	if (dom && ecx->state != ECX_UPDATING)
		dom->priv = ecm_strdup(empty_desc);
	return dom;
}

const char *
get_UPLOAD(ecx_t *ecx, const char *name)
{
	sr_t	*sr_UPLOADs, *sr_UPLOAD;

	sr_UPLOADs = sr_getc(ecx->info->sr_ecx_state, "uploads");
	sr_UPLOAD = sr_getc(sr_UPLOADs, ecx->STAGE);
	if (ecm_empty_str(name))
		return sr_geti_str(sr_first(sr_UPLOAD), 0);
	else
		return sr_geti_str(sr_getc(sr_UPLOAD, name), 0);
}

ECXDOM_t
FILE_BROWSE(ecx_t *ecx, const char *name, const char *empty_desc)
{
	ecx_dom_t	*dom;

	dom = create_dom(ecx, DOM_FILE_BROWSE, name);
	if (dom && ecx->state != ECX_UPDATING)
		dom->priv = ecm_strdup(empty_desc);
	return dom;
}

ECMBOOL
DOWNLOAD_BEGIN(ecx_t *ecx, ebc_ndid_t ndid, const char *path, const char *fname)
{
	sr_t	*sr_downloads, *sr_download;
	char	*href, *esc_fname = NULL;

	sr_downloads = sr_getc(ecx->info->sr_ecx_state, "DOWNLOADS");
	sr_download = sr_create_dir();
	sr_append(sr_download, sr_create_int(ST_NDID, ndid));
	sr_append(sr_download, sr_create_str(ST_STR, path));
	sr_append(sr_downloads, sr_download);
	if (!ecm_empty_str(fname))
		esc_fname = escape_str(fname);
	ecm_asprintf(&href, "/download?DOWNLOAD=%d&FNAME=%s", sr_get_num_child(sr_downloads), ecm_safe_str(esc_fname));
	ANCHOR_BEGIN(ecx, NULL, href);
	ecm_free(href);
	ecm_free(esc_fname);
	return ECM_TRUE;
}

void
DOWNLOAD_END(ecx_t *ecx)
{
	ANCHOR_END(ecx);
}

ECMBOOL
DOWNLOAD(ecx_t *ecx, ebc_ndid_t ndid, const char *path)
{
	sr_t	*sr_download;

	if (ecx->info->sr_download)
		return ECM_FALSE;

	if (ndid == self_ndid)
		ndid = EBC_NDID_SELF;
	sr_download = sr_create_dir();
	sr_append(sr_download, sr_create_int(ST_NDID, ndid));
	sr_append(sr_download, sr_create_str(ST_STR, path));

	ecx->info->sr_download = sr_download;
	return ECM_TRUE;
}

ECXDOM_t
TABLE_BEGIN(ecx_t *ecx, ECXATTR_t ATTR)
{
	ecx_dom_t	*dom;

	if ((dom = create_dom(ecx, DOM_TABLE, NULL)) == NULL)
		return NULL;
	dom->ATTR = ATTR;
	return dom;
}

void
TABLE_END(ecx_t *ecx)
{
	pop_current_dom(ecx->info, DOM_TABLE);
}

ECXDOM_t
TR(ecx_t *ecx, ECXATTR_t ATTR)
{
	ecx_dom_t	*dom;

	if ((dom = create_dom(ecx, DOM_TR, NULL)) == NULL)
		return NULL;
	dom->ATTR = ATTR;
	return dom;
}

ECXDOM_t
TD(ecx_t *ecx, ECXATTR_t ATTR)
{
	ecx_dom_t	*dom;

	if ((dom = create_dom(ecx, DOM_TD, NULL)) == NULL)
		return NULL;
	dom->ATTR = ATTR;
	return dom;
}

ECXDOM_t
DIV_BEGIN(ecx_t *ecx)
{
	return create_dom(ecx, DOM_DIV, NULL);
}

void
DIV_END(ecx_t *ecx)
{
	pop_current_dom(ecx->info, DOM_DIV);
}
